package skyproc;

import java.io.*;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.zip.DataFormatException;
import lev.*;
import skyproc.SPGlobal.Language;
import skyproc.SubStringPointer.Files;
import skyproc.exceptions.BadMod;
import skyproc.exceptions.BadParameter;
import skyproc.exceptions.BadRecord;
import skyproc.gui.SPProgressBarPlug;

/**
 * A mod is a collection of GRUPs which contain records. Mods are used to create
 * patches by creating an empty one, adding records, and then calling its export
 * function.
 *
 * @author Justin Swanson
 */
public class Mod implements Comparable, Iterable<GRUP> {

    TES4 tes = new TES4();
    ModListing modInfo;
    Map<GRUP_TYPE, GRUP> GRUPs = new EnumMap<>(GRUP_TYPE.class);
    LInChannel input;
    Language language = Language.English;
    Map<ModListing, Integer> masterMap = new HashMap<>();
    Map<SubStringPointer.Files, Map<Integer, Integer>> stringLocations = new EnumMap<>(SubStringPointer.Files.class);
    Map<SubStringPointer.Files, LImport> stringStreams = new EnumMap<>(SubStringPointer.Files.class);
    private ArrayList<String> outStrings = new ArrayList<>();
    private ArrayList<String> outDLStrings = new ArrayList<>();
    private ArrayList<String> outILStrings = new ArrayList<>();

    /**
     * Creates an empty Mod with the name and master flag set to match info.
     *
     * @see ModListing
     * @param info ModListing object containing name and master flag.
     */
    public Mod(ModListing info) {
	init(info);
	SPGlobal.getDB().add(this);
    }

    Mod(ModListing info, ByteBuffer headerInfo) throws Exception {
	this(info, true);
	SPGlobal.logMod(this, "MOD", "Parsing header");
	if (!headerInfo.hasRemaining()) {
	    throw new BadMod(info.print() + " did not have a TES4 header.");
	}
	tes.parseData(headerInfo, this);
    }

    Mod(ModListing info, boolean temp) {
	init(info);
    }

    void deleteStringsFiles() {
	File STRINGS = new File(genStringsPath(SubStringPointer.Files.STRINGS));
	File DLSTRINGS = new File(genStringsPath(SubStringPointer.Files.STRINGS));
	File ILSTRINGS = new File(genStringsPath(SubStringPointer.Files.STRINGS));
	if (STRINGS.exists()) {
	    STRINGS.delete();
	}
	if (DLSTRINGS.exists()) {
	    DLSTRINGS.delete();
	}
	if (ILSTRINGS.exists()) {
	    ILSTRINGS.delete();
	}
    }

    void addGRUP(MajorRecord r) {
	GRUPs.put(GRUP_TYPE.valueOf(r.getType()), new GRUP(r));
    }

    void addGRUPrecursive(MajorRecord r) {
	GRUPs.put(GRUP_TYPE.valueOf(r.getType()), new GRUPRecursive(r));
    }

    final void init(ModListing info) {
	this.modInfo = info;
	this.setFlag(Mod_Flags.MASTER, info.getMasterTag());
	this.setFlag(Mod_Flags.STRING_TABLED, false);
	stringLocations.put(SubStringPointer.Files.STRINGS, new TreeMap<Integer, Integer>());
	stringLocations.put(SubStringPointer.Files.DLSTRINGS, new TreeMap<Integer, Integer>());
	stringLocations.put(SubStringPointer.Files.ILSTRINGS, new TreeMap<Integer, Integer>());
	addGRUP(new GMST());
	addGRUP(new KYWD());
	addGRUP(new TXST());
	addGRUP(new GLOB());
	addGRUP(new FACT());
	addGRUP(new HDPT());
	addGRUP(new RACE());
	addGRUP(new MGEF());
	addGRUP(new ENCH());
	addGRUP(new SPEL());
        addGRUP(new SCRL());
	addGRUP(new ARMO());
	addGRUP(new BOOK());
	addGRUP(new CONT());
	addGRUP(new INGR());
	addGRUP(new MISC());
	addGRUP(new ALCH());
	addGRUP(new COBJ());
	addGRUP(new PROJ());
	addGRUP(new STAT());
	addGRUP(new WEAP());
	addGRUP(new AMMO());
	addGRUP(new NPC_());
	addGRUP(new LVLN());
	addGRUP(new LVLI());
	addGRUP(new WTHR());
	addGRUPrecursive(new DIAL());
	addGRUP(new QUST());
	addGRUP(new IMGS());
	addGRUP(new FLST());
	addGRUP(new PERK());
	addGRUP(new VTYP());
	addGRUP(new AVIF());
	addGRUP(new ARMA());
	addGRUP(new ECZN());
	addGRUP(new LGTM());
	addGRUP(new DLBR());
	addGRUP(new DLVW());
	addGRUP(new OTFT());
    }

    /**
     * Creates an empty Mod with the name and master flag set to parameters.
     *
     * @param name String to set the Mod name to.
     * @param master Sets the Mod's master flag (which appends ".esp" and ".esm"
     * to the modname as appropriate)
     */
    public Mod(String name, Boolean master) {
	this(new ModListing(name, master));
    }

    /**
     * Returns the ModListing associated with the nth master of this mod.
     * Changing values on the returned ModListing will affect the mod it is tied
     * to.
     *
     * @param i The index of the master to return.
     * @return The ModListing object associated with the Nth master
     */
    public ModListing getNthMaster(int i) {
	if (tes.getMasters().size() > i && i >= 0) {
	    return tes.getMasters().get(i);
	} else {
	    return getInfo();
	}
    }

    /**
     *
     * @return The number of masters in the mod.
     */
    public int numMasters() {
	return tes.getMasters().size();
    }

    /**
     *
     * @return True if no GRUP in the mod has any records.
     */
    public boolean isEmpty() {
	for (GRUP g : GRUPs.values()) {
	    if (g.numRecords() > 0) {
		return false;
	    }
	}
	return true;
    }

    /**
     *
     * @return True if the esp file exists for this mod in the data folder.
     */
    public boolean exists() {
	File f = new File(SPGlobal.pathToData + getInfo().print());
	return f.exists();
    }

    FormID getNextID() {
	return new FormID(tes.getHEDR().nextID++, getInfo());
    }

    void resetNextIDcounter() {
	tes.getHEDR().nextID = Mod.HEDR.firstAvailableID;
    }

    void addMaster(ModListing input) {
	if (!getInfo().equals(input)) {
	    masterMap.clear();
	    tes.addMaster(input);
	}
    }

    void closeStreams() {
	input.close();
	for (LImport c : stringStreams.values()) {
	    c.close();
	}
    }

    /**
     *
     * @param query FormID to look in the mod for.
     * @param grup_types Types of GRUPs to look in. (Optional - searches all if
     * left blank)
     * @return The Major Record with the query FormID, or null if not found.
     */
    public MajorRecord getMajor(FormID query, GRUP_TYPE... grup_types) {
	if (query != null && query.getMaster() != null) {
	    for (GRUP_TYPE g : grup_types) {
		if (!GRUP_TYPE.internal(g)) {
		    GRUP grup = GRUPs.get(g);
		    MajorRecord mr = (MajorRecord) grup.get(query);
		    if (mr != null) {
			return mr;
		    }
		}
	    }
	}
	return null;
    }

    /**
     *
     * @param edid EDID to look in the mod for.
     * @param grup_types Types of GRUPs to look in. (Optional - searches all if
     * left blank)
     * @return The Major Record with the query FormID, or null if not found.
     */
    public MajorRecord getMajor(String edid, GRUP_TYPE... grup_types) {
	if (edid != null) {
	    for (GRUP_TYPE g : grup_types) {
		GRUP grup = GRUPs.get(g);
		MajorRecord mr = (MajorRecord) grup.get(edid);
		if (mr != null) {
		    return mr;
		}
	    }
	}
	return null;
    }

    /**
     * Makes a copy of the Major Record and loads it into the mod, giving a new
     * Major Record a FormID originating from the mod. This function also
     * automatically adds the new copied record to the mod. This makes two
     * separate records independent of each other.<br><br>
     *
     * CONSISTENCY NOTE: This functions appends "_DUP" to the end of the EDID,
     * and then a random number if that EDID already exists. It is suggested you
     * only use this function if you are only making one duplicate of the
     * record. For multiple duplicates, use the version with a specified EDID,
     * for better consistencyFile results<br><br>
     *
     * COMPILER NOTE: The record returned can only be determined by the compiler
     * to be a Major Record. You must cast it yourself to be the correct type of
     * major record.<br> ex. NPC_ newNPC = (NPC_) myPatch.makeCopy(otherNPC);
     *
     * @param m Major Record to make a copy of and add to the mod.
     * @return The copied record.
     */
    MajorRecord makeCopy(MajorRecord m) {
	m = m.copyOf(this);
	GRUPs.get(GRUP_TYPE.valueOf(m.getType())).addRecord(m);
	return m;
    }

    /**
     * Makes a copy of the Major Record and loads it into the mod, giving a new
     * Major Record a FormID originating from the mod. This function also
     * automatically adds the new copied record to the mod. This makes two
     * separate records independent of each other.<br><br>
     *
     * COMPILER NOTE: The record returned can only be determined by the compiler
     * to be a Major Record. You must cast it yourself to be the correct type of
     * major record.<br> ex. NPC_ newNPC = (NPC_) myPatch.makeCopy(otherNPC);
     *
     * @param m Major Record to make a copy of and add to the mod.
     * @param newEDID EDID to assign to the new record. Make sure it's unique.
     * @return The copied record, or null if either parameter is null.
     */
    public MajorRecord makeCopy(MajorRecord m, String newEDID) {
	if (m == null || newEDID == null) {
	    return null;
	}
	m = m.copyOf(this, newEDID);
	GRUPs.get(GRUP_TYPE.valueOf(m.getType())).addRecord(m);
	return m;
    }

    /**
     * This function requires there to be a GlobalDB set, as it adds the
     * necessary masters from it.<br><br>
     *
     * @param g GRUP to make a copy of.
     * @return ArrayList of duplicated Major Records.
     */
    public ArrayList<MajorRecord> makeCopy(GRUP g) {
	ArrayList<MajorRecord> out = new ArrayList<>();
	for (Object o : g) {
	    MajorRecord m = (MajorRecord) o;
	    out.add(makeCopy(m));
	}
	return out;
    }

    /**
     *
     * @return An arraylist containing the GRUP_TYPEs of non-empty GRUPs.
     */
    public ArrayList<GRUP_TYPE> getContainedTypes() {
	ArrayList<GRUP_TYPE> out = new ArrayList<>();
	for (GRUP g : GRUPs.values()) {
	    if (!g.isEmpty()) {
		out.add(g.getContainedType());
	    }
	}
	return out;
    }

    /**
     *
     * This function requires there to be a GlobalDB set, as it adds the
     * necessary masters from it.
     *
     * @param m Major Record to add as an override.
     */
    public void addRecord(MajorRecord m) {
	GRUPs.get(GRUP_TYPE.valueOf(m.getType())).addRecord(m);
    }

    /**
     * Prints each GRUP in the mod to the asynchronous log.
     */
    public void print() {

	SPGlobal.newSyncLog("Mod Export/" + getName() + ".txt");

	if (!getMastersStrings().isEmpty()) {
	    SPGlobal.logMod(this, getName(), "=======================================================================");
	    SPGlobal.logMod(this, getName(), "======================= Printing Mod Masters ==========================");
	    SPGlobal.logMod(this, getName(), "=======================================================================");
	    for (String s : getMastersStrings()) {
		SPGlobal.logMod(this, getName(), s);
	    }
	}
	for (GRUP g : GRUPs.values()) {
	    g.toString();
	}
	SPGlobal.logMod(this, getName(), "------------------------  DONE PRINTING -------------------------------");
    }

    /**
     *
     * @return
     */
    @Override
    public String toString() {
	return getName();
    }

    ArrayList<FormID> allFormIDs() {
	ArrayList<FormID> tmp = new ArrayList<>();
	for (GRUP g : GRUPs.values()) {
	    tmp.addAll(g.allFormIDs());
	}
	ArrayList<FormID> out = new ArrayList<>(tmp.size());
	for (FormID id : tmp) {
	    if (id != null) {
		out.add(id);
	    } else {
		SPGlobal.logError(this.toString(), "AllFormIDs return null formid reference.");
	    }
	}
	return out;
    }

    void openStringStreams() {
	if (this.isFlag(Mod_Flags.STRING_TABLED)) {
	    for (Files f : SubStringPointer.Files.values()) {
		addStream(stringStreams, f);
	    }
	}
    }

    void addStream(Map<SubStringPointer.Files, LImport> streams, SubStringPointer.Files file) {
	try {
	    String stringPath = SPImporter.getStringFilePath(this, language, file);
	    File stringFile = new File(SPGlobal.pathToData + stringPath);
	    if (stringFile.isFile()) {
		streams.put(file, new LInChannel(stringFile));
		return;
	    } else if (BSA.hasBSA(getInfo())) {
		BSA bsa = BSA.getBSA(getInfo());
		if (bsa.hasFile(stringPath)) {
		    LByteChannel stream = new LByteChannel();
		    stream.openStream(bsa.getFile(stringPath));
		    streams.put(file, stream);
		}
		return;
	    }

	    if (SPGlobal.logging()) {
		SPGlobal.logMod(this, getName(), "No strings file for " + file);
	    }
	} catch (IOException | DataFormatException ex) {
	    SPGlobal.logMod(this, getName(), "Could not open a strings stream for mod " + getName() + " to type: " + file);
	}
    }

    /**
     *
     * @see ModListing
     * @return The names of all the masters of the mod.
     */
    public ArrayList<String> getMastersStrings() {
	ArrayList<String> out = new ArrayList<>();
	for (ModListing m : tes.getMasters()) {
	    out.add(m.print());
	}
	return out;
    }

    /**
     * Returns the ModListings of all the masters of the mod. Note that changing
     * any ModListings will have an effect on their associated mods.
     *
     * @see ModListing
     * @return The ModListings of all the masters of the mod.
     */
    public ArrayList<ModListing> getMasters() {
	ArrayList<ModListing> out = new ArrayList<>();
	for (ModListing m : tes.getMasters()) {
	    out.add(m);
	}
	return out;
    }

    /**
     *
     * @return the Map of GRUPs contained in the record.
     */
    public Map<GRUP_TYPE, GRUP> getGRUPs() {
	return GRUPs;
    }

    /**
     * Clears all GRUPS in the Mod except for the GRUPs specified in the
     * parameter.
     *
     * @param grup_type Any amount of GRUPs to keep, separated by commas
     */
    public void keep(GRUP_TYPE... grup_type) {
	ArrayList<GRUP_TYPE> grups = new ArrayList<>();
	grups.addAll(Arrays.asList(grup_type));
	for (GRUP g : GRUPs.values()) {
	    if (!grups.contains(g.getContainedType())) {
		g.clear();
	    }
	}
    }

    /**
     * This function will merge all GRUPs from the rhs mod into the calling
     * mod.<br> Any records already in the mod will be overwritten by the
     * version from rhs.<br><br> NOTE: Merging will NOT add records from a mod
     * with a matching ModListing. This is to prevent patches taking in
     * information from old versions of themselves.
     *
     * @param rhs Mod whose GRUPs to add in.
     * @param grup_types Any amount of GRUPs to merge in, separated by commas.
     * Leave this empty if you want all GRUPs merged.
     */
    public void addAsOverrides(Mod rhs, GRUP_TYPE... grup_types) {
	if (!this.equals(rhs)) {
	    if (grup_types.length == 0) {
		grup_types = GRUP_TYPE.values();
	    }
	    ArrayList<GRUP_TYPE> grups = new ArrayList<>(Arrays.asList(grup_types));
	    for (GRUP_TYPE t : grups) {
		GRUP g = GRUPs.get(t);
		if (g != null) {
		    g.merge(rhs.GRUPs.get(t));
		}
	    }
	}
    }

    /**
     * Iterates through each mod in the ArrayList, in order, and merges them in
     * one by one.<br> This means any conflicting records within the list will
     * end up with the last mod's version.<br><br> NOTE: Merging will NOT add
     * records from a mod with a matching ModListing. This is to prevent patches
     * taking in information from old versions of themselves.
     *
     * @param in ArrayList of mods to merge in.
     * @param grup_types Any amount of GRUPs to merge in, separated by commas.
     * Leave this empty if you want all GRUPs merged.
     */
    public void addAsOverrides(ArrayList<Mod> in, GRUP_TYPE... grup_types) {
	for (Mod m : in) {
	    addAsOverrides(m, grup_types);
	}
    }

    /**
     * NOTE: To sort the mods in load order, use a TreeSet.<br> Iterates through
     * each mod in the Set and merges them in one by one.<br> This means any
     * conflicting records within the set will end up with the last mod's
     * version.<br><br> NOTE: Merging will NOT add records from a mod with a
     * matching ModListing. This is to prevent patches taking in information
     * from old versions of themselves.
     *
     * @param in Set of mods to merge in.
     * @param grup_types Any amount of GRUPs to merge in, separated by commas.
     * Leave this empty if you want all GRUPs merged.
     */
    public void addAsOverrides(Collection<Mod> in, GRUP_TYPE... grup_types) {
	for (Mod m : in) {
	    addAsOverrides(m, grup_types);
	}
    }

    /**
     * Iterates through each mod in the SPDatabase, in load order, and merges
     * them in one by one.<br> This means any conflicting records within the
     * database will end up with the last mod's version.<br><br> NOTE: Merging
     * will NOT add records from a mod with a matching ModListing. This is to
     * prevent patches taking in information from old versions of themselves.
     *
     * @param db The SPDatabase to merge in.
     * @param grup_types Any amount of GRUPs to merge in, separated by commas.
     * Leave this empty if you want all GRUPs merged.
     */
    public void addAsOverrides(SPDatabase db, GRUP_TYPE... grup_types) {
	addAsOverrides(db.modLookup.values(), grup_types);
    }

    /**
     *
     * @return The number of records contained in all the GRUPs in the mod.
     */
    public int numRecords() {
	int sum = 0;
	for (GRUP g : GRUPs.values()) {
	    if (g.numRecords() != 0) {
		sum += g.numRecords() + 1;
	    }
	}
	return sum;
    }

    /**
     *
     * @return All Major Records from the mod.
     */
    public ArrayList<MajorRecord> getRecords() {
	ArrayList<MajorRecord> out = new ArrayList<>();
	for (GRUP g : GRUPs.values()) {
	    out.addAll(g.getRecords());
	}
	return out;
    }

    /**
     *
     * @param id FormID to look for.
     * @return True if mod contains parameter id.
     */
    public boolean contains(FormID id) {
	for (GRUP g : GRUPs.values()) {
	    if (g.contains(id)) {
		return true;
	    }
	}
	return false;
    }

    int getMasterIndex(ModListing in) {
	Integer out = masterMap.get(in);
	if (out != null) {
	    return out;
	} else {
	    ArrayList<ModListing> masters = getMasters();
	    int i;
	    for (i = 0; i < masters.size(); i++) {
		if (masters.get(i).equals(in)) {
		    return i;
		}
	    }
	    masterMap.put(in, i);
	    return i;
	}
    }

    /**
     * Exports the mod to the path designated by SPGlobal.pathToData.
     *
     * @see SPGlobal
     * @throws IOException If there are any unforseen disk errors exporting the
     * data.
     * @throws BadRecord If duplicate EDIDs are found in the mod. This has been
     * deemed an unacceptable mod format, and is thrown to promote the
     * investigation and elimination of duplicate EDIDs.
     */
    public void export() throws IOException, BadRecord {
	export(SPGlobal.pathToData);
    }

    /**
     * Exports the mod to the path given by the parameter. (You shouldn't
     * include the mod name in the string)
     *
     * @param path
     * @throws IOException
     * @throws BadRecord If duplicate EDIDs are found in the mod. This has been
     * deemed an unacceptable mod format, and is thrown to promote the
     * investigation and elimination of duplicate EDIDs.
     */
    public void export(String path) throws IOException, BadRecord {
	File tmp = new File(SPGlobal.pathToInternalFiles + "tmp.esp");
	if (tmp.isFile()) {
	    tmp.delete();
	}
	File dest = new File(path + getName());
	File backup = new File(SPGlobal.pathToInternalFiles + getName() + ".bak");
	if (backup.isFile()) {
	    backup.delete();
	}
	export(tmp);

	Ln.moveFile(dest, backup, false);
	Ln.moveFile(tmp, dest, false);
    }

    void export(File outPath) throws IOException, BadRecord {
	SPGlobal.logMain("Mod Export", "Exporting " + this);
	if (SPGlobal.logging()) {
	    SPGlobal.newSyncLog("Export - " + this.getName() + ".txt");
	    SPGlobal.sync(true);
	}

	ModExporter out = new ModExporter(outPath, this);

	ArrayList<GRUP> exportGRUPs = new ArrayList<>();

	// Progress Bar Setup
	for (GRUP g : GRUPs.values()) {
	    if (!g.isEmpty()) {
		exportGRUPs.add(g);
	    }
	}
	SPProgressBarPlug.reset();
	SPProgressBarPlug.setMax(exportGRUPs.size() + 6, "Exporting " + this);
	ArrayList<FormID> allForms = allFormIDs();

	// Add all mods that contained any of the FormIDs used.
	SPProgressBarPlug.setStatusNumbered("Adding Masters From Records");
	Set<ModListing> addedMods = new HashSet<>();
	for (FormID ID : allForms) {
	    if (!ID.isNull()) {
		ModListing master = ID.getMaster();
		if (!addedMods.contains(master)) {
		    addMaster(master);
		    addedMods.add(master);
		}
	    }
	}
	SPProgressBarPlug.incrementBar();

	// Go through each record, and add all mods that reference that record
	// Just to symbolize that they "had part" in the patch
	// And help encourage repatching when mods are removed.
	SPProgressBarPlug.setStatusNumbered("Adding Masters From Contributors");
	if (!SPGlobal.mergeMode) {
	    for (GRUP<MajorRecord> g : this) {
		for (MajorRecord major : g) {
		    FormID id = major.getForm();
		    for (Mod mod : SPGlobal.getDB().getImportedMods()) {
			if (mod.contains(id) && !addedMods.contains(mod.getInfo())
				&& !mod.equals(SPGlobal.getGlobalPatch())) {
			    addMaster(mod.getInfo());
			    addedMods.add(mod.getInfo());
			}
		    }
		}
	    }
	}
	SPProgressBarPlug.incrementBar();

	// Sort masters to match load order
	sortMasters();
	SPProgressBarPlug.incrementBar();

	// Export Header
	tes.setNumRecords(numRecords());
	if (SPGlobal.logging()) {
	    SPGlobal.logSync(this.getName(), "Exporting " + tes.getHEDR().numRecords + " records.");
	    SPGlobal.logSync(this.getName(), "Masters: ");
	    int i = 0;
	    for (String s : this.getMastersStrings()) {
		SPGlobal.logSync(this.getName(), "   " + Ln.printHex(i++) + "  " + s);
	    }
	}

	tes.export(out);

	// Export GRUPs
	for (GRUP g : exportGRUPs) {
	    SPProgressBarPlug.setStatusNumbered("Exporting " + this + ": " + g.getContainedType());
	    g.export(out);
	    SPProgressBarPlug.incrementBar();
	}

	// Export or clean up STRINGS files
	if (this.isFlag(Mod_Flags.STRING_TABLED)) {
	    SPProgressBarPlug.setStatusNumbered("Exporting " + this + ": STRINGS files");
	    exportStringsFile(outStrings, SubStringPointer.Files.STRINGS);
	    exportStringsFile(outDLStrings, SubStringPointer.Files.DLSTRINGS);
	    exportStringsFile(outILStrings, SubStringPointer.Files.ILSTRINGS);
	    SPProgressBarPlug.incrementBar();
	} else {
	    deleteStringsFiles();
	}
	out.close();


	// Check if any duplicate EDIDS or FormIDS
	SPProgressBarPlug.setStatusNumbered("Checking for Duplicates");
	Map<String, MajorRecord> edids = new HashMap<>();
	Set<FormID> IDs = new HashSet<>();
	boolean bad = false;
	for (GRUP g : GRUPs.values()) {
	    for (Object o : g.listRecords) {
		MajorRecord m = (MajorRecord) o;
		if (edids.keySet().contains(m.getEDID())
			&& (m.getFormMaster().equals(SPGlobal.getGlobalPatch().modInfo)
			|| edids.get(m.getEDID()).getFormMaster().equals(SPGlobal.getGlobalPatch().modInfo))) {
		    SPGlobal.logError("EDID Check", "Error! Duplicate EDID " + m);
		    SPGlobal.logError("EDID Check", "    With: " + edids.get(m.getEDID()));
		    bad = true;
		} else {
		    edids.put(m.getEDID(), m);
		}
		if (IDs.contains(m.getForm())) {
		    SPGlobal.logError("FormID Check", "Error! Duplicate FormID " + m);
		    bad = true;
		} else {
		    IDs.add(m.getForm());
		}
	    }
	}
	SPProgressBarPlug.incrementBar();
	if (bad) {
	    throw new BadRecord("Duplicate EDIDs or FormIDs.  Check logs for a listing.");
	}

	SPProgressBarPlug.setStatusNumbered("Validating Record Lengths");
	// Validate all record lengths are correct
	if (!NiftyFunc.validateRecordLengths(outPath, 1)) {
	    SPGlobal.logError("Record Length Check", "Record lengths were off.");
	    throw new BadRecord("Record lengths are off.");
	}
	SPProgressBarPlug.incrementBar();

	SPProgressBarPlug.setStatusNumbered("Exporting Consistency File");
	if (Consistency.automaticExport) {
	    Consistency.export();
	}
	SPProgressBarPlug.incrementBar();
    }

    /**
     * Exports the master list of this mod to "Files/Last Masterlist.txt"<br>
     * Used for checking if patches are needed.
     *
     * @param path
     * @throws IOException
     */
    public void exportMasterList(String path) throws IOException {
	File masterListTmp = new File(SPGlobal.pathToInternalFiles + "Last Masterlist Temp.txt");
	BufferedWriter writer = new BufferedWriter(new FileWriter(masterListTmp));
	for (ModListing m : this.getMasters()) {
	    writer.write(m.toString() + "\n");
	}
	writer.close();

	File masterList = new File(path);
	if (masterList.isFile()) {
	    masterList.delete();
	}

	Ln.moveFile(masterListTmp, masterList, false);
    }

    int addOutString(String in, SubStringPointer.Files file) {
	switch (file) {
	    case DLSTRINGS:
		return addOutString(in, outDLStrings);
	    case ILSTRINGS:
		return addOutString(in, outILStrings);
	    default:
		return addOutString(in, outStrings);
	}
    }

    int addOutString(String in, ArrayList<String> list) {
	if (!list.contains(in)) {
	    list.add(in);
	}
	return list.indexOf(in) + 1;  // To prevent indexing starting at 0
    }

    String genStringsPath(SubStringPointer.Files file) {
	return SPGlobal.pathToData + "Strings/" + getNameNoSuffix() + "_" + SPGlobal.language + "." + file;
    }

    void sortMasters() {
	tes.getMasters().sort();
    }

    void exportStringsFile(ArrayList<String> list, SubStringPointer.Files file) throws FileNotFoundException, IOException {
	int stringLength = 0;
	for (String s : list) {
	    stringLength += s.length() + 1;

	}
	int outLength = stringLength + 8 * list.size() + 8;
	LOutFile out = new LOutFile(genStringsPath(file));

	out.write(list.size());
	out.write(stringLength);

	int i = 1;  // To prevent indexing starting at 0
	int offset = 0;
	for (String s : list) {
	    out.write(i++);
	    out.write(offset);
	    offset += s.length() + 1;
	}

	for (String s : list) {
	    out.write(s);
	    out.write(0, 1);  // Null terminator
	}

	list.clear();
	out.close();
    }

    /**
     * Sets the author name of the mod.
     *
     * @param in Your name here.
     */
    public void setAuthor(String in) {
	tes.setAuthor(in);
    }

    void parseData(String type, LImport data) throws Exception {
	GRUPs.get(GRUP_TYPE.valueOf(type)).parseData(data, this);
    }

    /**
     * Returns whether the given flag is on or off. <br> <br> An example use of
     * this function is as follows: <br> boolean isaMasterMod =
     * myMod.isFlag(Mod_Flags.MASTER);
     *
     * @see Mod_Flags
     * @param flag Mod_Flags enum to check.
     * @return True if the given flag is on in the mod.
     */
    public boolean isFlag(Mod_Flags flag) {
	return tes.flags.get(flag.value);
    }

    /**
     * Sets the given flag in the mod. <br> <br> An example of using this
     * function to set its master flag is as follows: <br>
     * myMod.setFlag(Mod_Flags.MASTER, true);
     *
     * @see Mod_Flags
     * @param flag Mod_Flags enum to set.
     * @param on What to set the flag to.
     */
    public final void setFlag(Mod_Flags flag, boolean on) {
	tes.flags.set(flag.value, on);
	if (flag == Mod_Flags.MASTER) {
	    getInfo().setMasterTag(on);
	}
    }

    // Get Set
    /**
     *
     * @see GRUP
     * @return The GRUP containing Leveled List records.
     */
    public GRUP<LVLN> getLeveledCreatures() {
	return GRUPs.get(GRUP_TYPE.LVLN);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing NPC records.
     */
    public GRUP<NPC_> getNPCs() {
	return GRUPs.get(GRUP_TYPE.NPC_);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Image Space records.
     */
    public GRUP<IMGS> getImageSpaces() {
	return GRUPs.get(GRUP_TYPE.IMGS);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Perk records.
     */
    public GRUP<PERK> getPerks() {
	return GRUPs.get(GRUP_TYPE.PERK);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Spell records.
     */
    public GRUP<SPEL> getSpells() {
	return GRUPs.get(GRUP_TYPE.SPEL);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Race records
     */
    public GRUP<RACE> getRaces() {
	return GRUPs.get(GRUP_TYPE.RACE);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Armor records
     */
    public GRUP<ARMO> getArmors() {
	return GRUPs.get(GRUP_TYPE.ARMO);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Armature records
     */
    public GRUP<ARMA> getArmatures() {
	return GRUPs.get(GRUP_TYPE.ARMA);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Texture records
     */
    public GRUP<TXST> getTextureSets() {
	return GRUPs.get(GRUP_TYPE.TXST);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Weapon records
     */
    public GRUP<WEAP> getWeapons() {
	return GRUPs.get(GRUP_TYPE.WEAP);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Keyword records
     */
    public GRUP<KYWD> getKeywords() {
	return GRUPs.get(GRUP_TYPE.KYWD);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Keyword records
     */
    public GRUP<FLST> getFormLists() {
	return GRUPs.get(GRUP_TYPE.FLST);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Magic Effect records
     */
    public GRUP<MGEF> getMagicEffects() {
	return GRUPs.get(GRUP_TYPE.MGEF);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Alchemy records
     */
    public GRUP<ALCH> getAlchemy() {
	return GRUPs.get(GRUP_TYPE.ALCH);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Ingredient records
     */
    public GRUP<INGR> getIngredients() {
	return GRUPs.get(GRUP_TYPE.INGR);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Ammo records
     */
    public GRUP<AMMO> getAmmo() {
	return GRUPs.get(GRUP_TYPE.AMMO);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Faction records
     */
    public GRUP<FACT> getFactions() {
	return GRUPs.get(GRUP_TYPE.FACT);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Game Setting records
     */
    public GRUP<GMST> getGameSettings() {
	return GRUPs.get(GRUP_TYPE.GMST);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Enchantments records
     */
    public GRUP<ENCH> getEnchantments() {
	return GRUPs.get(GRUP_TYPE.ENCH);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Leveled Items records
     */
    public GRUP<LVLI> getLeveledItems() {
	return GRUPs.get(GRUP_TYPE.LVLI);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing ActorValue records
     */
    public GRUP<AVIF> getActorValues() {
	return GRUPs.get(GRUP_TYPE.AVIF);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Encounter Zone records
     */
    public GRUP<ECZN> getEncounterZones() {
	return GRUPs.get(GRUP_TYPE.ECZN);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Global records
     */
    public GRUP<GLOB> getGlobals() {
	return GRUPs.get(GRUP_TYPE.GLOB);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Constructible Object records
     */
    public GRUP<COBJ> getConstructibleObjects() {
	return GRUPs.get(GRUP_TYPE.COBJ);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Misc Object records
     */
    public GRUP<MISC> getMiscObjects() {
	return GRUPs.get(GRUP_TYPE.MISC);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing outfit records
     */
    public GRUP<OTFT> getOutfits() {
	return GRUPs.get(GRUP_TYPE.OTFT);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing head part records
     */
    public GRUP<HDPT> getHeadParts() {
	return GRUPs.get(GRUP_TYPE.HDPT);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing projectile records
     */
    public GRUP<PROJ> getProjectiles() {
	return GRUPs.get(GRUP_TYPE.PROJ);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing lighting template records
     */
    public GRUP<LGTM> getLightingTemplates() {
	return GRUPs.get(GRUP_TYPE.LGTM);
    }

    /**
     *
     * @see GRUP
     * @return The GRUP containing Book records
     */
    public GRUP<BOOK> getBooks() {
	return GRUPs.get(GRUP_TYPE.BOOK);
    }

    /**
     *
     * @see GRUP
     * @return
     */
    public GRUP<WTHR> getWeathers() {
	return GRUPs.get(GRUP_TYPE.WTHR);
    }

    /**
     *
     * @see GRUP
     * @return
     */
    public GRUP<DIAL> getDialogs() {
	return GRUPs.get(GRUP_TYPE.DIAL);
    }

    /**
     *
     * @see GRUP
     * @return
     */
    public GRUP<DLBR> getDialogBranches() {
	return GRUPs.get(GRUP_TYPE.DLBR);
    }

    /**
     *
     * @see GRUP
     * @return
     */
    public GRUP<DLVW> getDialogViews() {
	return GRUPs.get(GRUP_TYPE.DLVW);
    }

    /**
     *
     * @see GRUP
     * @return
     */
    public GRUP<CONT> getContainers() {
	return GRUPs.get(GRUP_TYPE.CONT);
    }

    /**
     *
     * @see GRUP
     * @return
     */
    public GRUP<VTYP> getVoiceTypes() {
	return GRUPs.get(GRUP_TYPE.VTYP);
    }

    /**
     *
     * @see GRUP
     * @return
     */
    public GRUP<STAT> getStatics() {
	return GRUPs.get(GRUP_TYPE.STAT);
    }

    /**
     *
     * @return The name of the mod (including suffix)
     */
    public String getName() {
	return modInfo.print();
    }

    /**
     *
     * @see ModListing
     * @return The ModListing object associated with the mod.
     */
    public ModListing getInfo() {
	return modInfo;
    }

    /**
     *
     * @return The name of the mod (without suffix)
     */
    public String getNameNoSuffix() {
	return getName().substring(0, getName().indexOf(".es"));
    }

    /**
     * A compare function used for sorting Mods in load order. <br> .esp > .esm
     * <br> later date > earlier date <br>
     *
     * @param o Another Mod object
     * @return 1: Mod is later in the load order <br> -1: Mod is earlier in the
     * load order <br> 0: Mod is equal in load order (should not happen)
     */
    @Override
    public int compareTo(Object o) {
	Mod rhs = (Mod) o;
	return this.getInfo().compareTo(rhs.getInfo());
    }

    /**
     * An equals function that compares only mod name. It does NOT compare mod
     * contents.
     *
     * @param obj
     * @return True if obj is a mod with the same name and suffix (Skyrim.esm ==
     * Skyrim.esm)
     */
    @Override
    public boolean equals(Object obj) {
	if (obj == null) {
	    return false;
	}
	if (obj instanceof ModListing) {
	    return getInfo().equals(obj);
	}
	if (getClass() != obj.getClass()) {
	    return false;
	}
	final Mod other = (Mod) obj;
	if (!this.getName().equalsIgnoreCase(other.getName())) {
	    return false;
	}
	return true;
    }

    /**
     *
     * @param id
     */
    public void remove(FormID id) {
	for (GRUP g : GRUPs.values()) {
	    if (g.contains(id)) {
		g.removeRecord(id);
	    }
	}
    }

    /**
     * A custom hash function that takes the mod header into account for better
     * hashing.
     *
     * @return
     */
    @Override
    public int hashCode() {
	int hash = 5;
	hash = 97 * hash + (this.modInfo != null ? this.modInfo.hashCode() : 0);
	return hash;
    }

    /**
     *
     * @return An iterator over all the GRUPs in the mod.
     */
    @Override
    public Iterator<GRUP> iterator() {
	return GRUPs.values().iterator();
    }

    // Internal Classes
    static class TES4 extends Record {

	private final static byte[] defaultINTV = Ln.parseHexString("C5 26 01 00", 4);
	static final SubPrototype TES4proto = new SubPrototype() {
	    @Override
	    protected void addRecords() {
		add(new HEDR());
		add(SubString.getNew("CNAM", true));
		add(new SubList<>(new ModListing(), true));
		add(SubString.getNew("SNAM", true));
		add(new SubData("INTV"));
		add(new SubData("ONAM"));
		add(new SubData("INCC"));
	    }
	};
	SubRecordsDerived subRecords = new SubRecordsDerived(TES4proto);
	private LFlags flags = new LFlags(4);
	private int fluff1 = 0;
	private int fluff2 = 0;
	private int fluff3 = 0;

	TES4() {
	}

	TES4(LShrinkArray in) throws Exception {
	    this();
	    parseData(in, null);
	}

	@Override
	final void parseData(LImport in, Mod srcMod) throws BadRecord, BadParameter, DataFormatException {
	    super.parseData(in, srcMod);
	    flags.set(in.extract(4));
	    fluff1 = Ln.arrayToInt(in.extractInts(4));
	    fluff2 = Ln.arrayToInt(in.extractInts(4));
	    fluff3 = Ln.arrayToInt(in.extractInts(4));
	    subRecords.importSubRecords(in, srcMod);
	}

	@Override
	boolean isValid() {
	    return true;
	}

	@Override
	public String toString() {
	    return "HEDR";
	}

	@Override
	ArrayList<String> getTypes() {
	    return Record.getTypeList("TES4");
	}

	@Override
	void export(ModExporter out) throws IOException {
	    super.export(out);
	    out.write(flags.export(), 4);
	    out.write(fluff1);
	    out.write(fluff2);
	    out.write(fluff3);
	    subRecords.export(out);
	}

	void addMaster(ModListing mod) {
	    subRecords.getSubList("MAST").add(mod);
	}

	void clearMasters() {
	    subRecords.getSubList("MAST").clear();
	}

	SubList<ModListing, ModListing> getMasters() {
	    return subRecords.getSubList("MAST");
	}

	void setAuthor(String in) {
	    subRecords.getSubString("CNAM").setString(in);
	}

	@Override
	int getFluffLength() {
	    return 16;
	}

	@Override
	int getContentLength(ModExporter out) {
	    return subRecords.length(out);
	}

	@Override
	int getSizeLength() {
	    return 4;
	}

	void setNumRecords(int num) {
	    getHEDR().setRecords(num);
	}

	HEDR getHEDR() {
	    return (HEDR) subRecords.get("HEDR");
	}

	int getNumRecords() {
	    return getHEDR().numRecords;
	}

	@Override
	Record getNew() {
	    return new TES4();
	}

	@Override
	public String print() {
	    return toString();
	}
    }

    static class HEDR extends SubRecord<HEDR> {

	byte[] version;
	int numRecords;
	int nextID;
	static int firstAvailableID = 0xD62;  // first available ID on empty CS plugins

	HEDR() {
	    super();
	    clear();
	}

	HEDR(LShrinkArray in) throws Exception {
	    this();
	    parseData(in, null);
	}

	void setRecords(int num) {
	    numRecords = num;
	}

	int numRecords() {
	    return numRecords;
	}

	void addRecords(int num) {
	    setRecords(numRecords() + num);
	}

	int nextID() {
	    return nextID++;
	}

	@Override
	void export(ModExporter out) throws IOException {
	    super.export(out);
	    out.write(version);
	    out.write(numRecords);
	    out.write(nextID);
	}

	@Override
	final void parseData(LImport in, Mod srcMod) throws BadRecord, BadParameter, DataFormatException {
	    super.parseData(in, srcMod);
	    version = in.extract(4);
	    numRecords = in.extractInt(4);
	    nextID = in.extractInt(4);
	}

	@Override
	SubRecord getNew(String type) {
	    return new HEDR();
	}

	final public void clear() {
	    version = Ln.parseHexString("D7 A3 70 3F", 4);
	    numRecords = 0;
	    nextID = firstAvailableID;
	}

	@Override
	boolean isValid() {
	    return true;
	}

	@Override
	int getContentLength(ModExporter out) {
	    return 12;
	}

	@Override
	ArrayList<String> getTypes() {
	    return Record.getTypeList("HEDR");
	}
    }

    /**
     * The various flags found in the Mod header.
     */
    public enum Mod_Flags {

	/**
	 * Master flag determines whether a mod is labeled as a master plugin
	 * and receives a ".esm" suffix.
	 */
	MASTER(0),
	/**
	 * String Tabled flag determines whether names and descriptions are
	 * stored inside the mod itself, or in external STRINGS files. <br><br>
	 *
	 * In general, it is suggested to disable this flag for most patches, as
	 * the only benefit of external STRINGS files is easy multi-language
	 * support. Skyproc can offer this same multilanguage support without
	 * external STRINGS files. See SPGlobal.language for more information.
	 *
	 * @see SPGlobal
	 */
	STRING_TABLED(7);
	int value;

	private Mod_Flags(int in) {
	    value = in;
	}
    };
}
